	
	var MCFrame,MC,Toolbar,Selection,Clipboard
	
/* -------------------------------------------------------------------------------------------
CHROME

------------------------------------------------------------------------------------------- */
	
	Object.extend(Chrome, {
		// for user info
		name:'Smitten',
		version:0.1,
		// Particular to our CSS layout
		MCOffset:{x:150, y:86},
		// the mask element
		maskdiv:false,
		// The maincell element(s)
		mcs:['maincell'],
		// load json data, if any
		load:function(o){
			if(o){
				for(var k in o){
					switch(k){
						case 'Toolbar':
							Toolbar = new ToolbarBuilder();
							Toolbar.load(o[k]);
							break;
						case 'Selection':
							Selection = new SelectionHandler();
							Selection.load(o[k]);
							break;
						case 'Clipboard':
							Clipboard = new ClipboardHandler();
							Clipboard.load(o[k]);
							break;
						case 'maincell':
							this.mcs.push(o[k]);
							this.resize();
							break;
					}
				}
			}
			if(!this.loaded){
				this.loaded = true;
				if(this.tlb && !Toolbar) this.load({Toolbar:this.tlb});
				this.resize();
				Event.observe(window, 'resize', this.resize);
				Smernel.fire('chrome:loaded');
			}
		},
		resize:function(){
			var d = Chrome.MCDimensions()
			Chrome.rMainCells({width:d.width+'px', height:d.height +'px'});
		},
		unfillscreen:function(){
			removeEvent(window, 'resize', this.resize);
			Chrome.rMainCells({width:'100%', height:'100%'});
		},
		rMainCells:function(d){
			this.mcs.each(function(eid){
				$(eid).style.width = d.width;
				$(eid).style.height = d.height;
			});
		},
		MCDimensions:function(){
			return {width: window.getWidth() - Chrome.MCOffset.x, height:window.getHeight() - Chrome.MCOffset.y}
		},
		// Iframe = Chrome.mkIframe()
		mkMCFrame:function(o){
			if(typeof o !== 'object') o = {};
			MCFrame = mk('div');
			MCFrame.frameElement = MCFrame;
			MCFrame.body = MCFrame;
			$('maincell').appendChild(MCFrame);
			initMCFrame(MCFrame, {x:this.MCOffset.x, y:this.MCOffset.y-30});
			this.load({'maincell':MCFrame.id});
		},
		maskcallers:{},
		maskcalls:0,
		mask:function(caller){
			if(this.maskcallers[caller]) return;
			this.maskcallers[caller] = true;
			this.maskcalls++;
			if(this.maskcalls > 1) return;
			$('msk').show();
		},
		unmask:function(caller){
			if(!this.maskcallers[caller]) return;
			this.maskcallers[caller] = false;
			this.maskcalls--;
			if(this.maskcalls) return;
			$('msk').hide();
		},
		wait:function(){
			if($('tlb-ul0')) $('tlb-ul0').style.display = 'none';
			$('wait').show();
		},
		unwait:function(){
			if($('tlb-ul0')) $('tlb-ul0').style.display = 'block';
			$('wait').hide();
		}
	});
	
	Smernel.observe('wait', Chrome.wait);
	Smernel.observe('unwait', Chrome.unwait);
	if(Prototype.Browser.WebKit) window.prefix(function(){document.include_css(Smernel.chrome_path + '/css/webkit.css')});
	window.append(function(){if(!Chrome.loaded){Chrome.load()}});
	
/* -------------------------------------------------------------------------------------------
APPS

------------------------------------------------------------------------------------------- */
	var Apps = {
		length:0,
		apps:{},
		load:function(apps){
			this.length = 0;
			this.apps = {}
			this.add(apps)
		},
		add:function(apps){
			apps = this.filter(apps);
			for(var k in apps){
				this.apps[k] = new AppConstructor(apps[k], this);
				this.length++
			}
			this.loaded();
		},
		filter:function(a){ return a },
		loaded:function(){  },
		clickHandler:function(){},
		_each:function(iterator){
			for(var k in this.apps) iterator(this.apps[k]);
		},
		sortByMember:function(member, desc){
			var a = this.sortBy(function(b){ return b[member]; }), c = {};
			if(desc) a.reverse();
			for(var k in a) if(!Object.isFunction(a[k])) c[k] = a[k]
			this.apps = c;
		}
	}
	
	Object.extend(Apps, Enumerable);
	
	var AppConstructor = function(o, a){
		for(var k in o) this[k] = o[k]
		this.icon = (this.icon && this.icon.substr(0, 7) == 'http://') ? this.icon : Chrome.app_icon(this);
		this.click = a.clickHandler
	}

	
	
/* -------------------------------------------------------------------------------------------
IFRAME INIT (instantiates MC)

------------------------------------------------------------------------------------------- */
	
	// 	initIframe instantiates the Maincell object after iframe load.  
	// 	An iframe instantiated with src ./iframe.html passes its window var as first argument to any function it knows as onFrameLoad, such as this one
	var initMCFrame = function(win, offset){
		MC  = new MainCell(win, offset);
		window.MC = MC;
		if((Selection)&&(Selection.allow_multi)){
			var shiftCheck = function(e){ 
				Selection.multi = cmdTest(e);
				//alert(e.keyCode);e.preventDefault();
				if(!Selection.multi || e.keyCode != 65) return;
				e.preventDefault();
				MC.selectall()
			}
			addEvent(win, 'keydown', shiftCheck);
			addEvent(win, 'keyup', shiftCheck);
			addEvent(window, 'keydown', shiftCheck);
			addEvent(window, 'keyup', shiftCheck);
		}
		var MCBlur = function(e){ if(e.target == MC.body) MC.blur() }
		addEvent(MC.body, 'click', MCBlur);
		var MCResize = function(){ MC.resize() }
		addEvent(window, 'resize', MCResize);
		Chrome.unfillscreen();
	}
	

/* -------------------------------------------------------------------------------------------
MAINCELL

------------------------------------------------------------------------------------------- */
	
	var MainCell = function(win, offset){
		// iframe sub objects
		//this.window = win;
		this.iframe = win.frameElement;
		this.iframe.id = 'MainCellIframe';
		this.body = win.body;
		this.body.style.textAlign = 'left';
		this.body.style.padding = '5px';
		this.view = 0;

		// cur
		this.cur = {}
		this.cur.divs = {}
		this.cur.width = 0;
		this.cur.height = 0;
		this.offset = (offset) ? offset : {x:0,y:0}

		// Frame handling
		this.setFrameDims = function(){
			var dims = Chrome.MCDimensions()
			this.cur.height = dims.height;
			this.cur.width = dims.width;
			this.iframe.style.height = this.cur.height + 'px';
			this.iframe.style.width = this.cur.width + 'px';
		}
		// since we've defined the function and Files should be full, let's set the dims
		this.setFrameDims();

		// xy : calculates maximum number of files that may be fitted into current dimensions.
		this.xy = function(){
			var w = this.cur.width - 20;
			var h = this.cur.height - 20;
			// maximum divs we can fit horizontally
			var mx = (w - (w % 84)) / 84;
			// maximum divs we can fit vertically
			var my = (h - (h % 84)) / 84;
			// maximum divs we can fit on screen
			var max = mx * my;
			// is there enough space? if not, expand y
			var l = Apps.length;
			if (l > (mx * my)) my = my + parseInt((l-max)/mx) + 1 ;
			return {x:mx, y:my}
		}

		// draw : creates new div elements, adds their event listeners and appends them to body
		this.draw = function(delay){
			this.body.innerHTML = '';
			this.table = false;
			this.body.style.display = 'none';
			this.cur.divs = {};
			var i = 0;
			if(!this.view) var xy = this.xy();
			for(var k in Apps.apps){
				switch(this.view){
					case 0:
						var x = (i < xy.y) ? 0 : parseInt(i/xy.y);
						var y = (i < xy.y) ? i : (i % xy.y);
						var el = this.cur.divs[k] = MainCell.mkIconDiv(Apps.apps[k], x, y, this);
						this.body.appendChild(el);
						break;
					case 1:
						var el = this.cur.divs[k] = MainCell.mkAppTr(Apps.apps[k], this, i);
				}
				addEvent(el, 'mouseover', MainCell.divOnOver);
				addEvent(el, 'mouseout', MainCell.divOnOut);
				el.click = MainCell.divOnClick;
				addEvent(el, 'click', el.click);
				i++;
			}
			// We set a delay on page load to give the browser time to sort the css out (avoids ugly streaky flash on firefox 3.1)
			if(delay) setTimeout(function(){ MC.body.style.display = 'block'; }, 100);
			else this.body.style.display = 'block';
			this.drawn();
		}

		// redraw : calls draw and resets classes to as they were
		this.redraw = function(){
			this.draw();
			if(Selection){
				for(var k in Selection.selected){
					if(!this.cur.divs[k]) Selection.remove(k);
					else this.cur.divs[k].className = this.cur.divs[k].className.split('-')[0] + '-sel';
				}
			}	
		}

		// blur : deselects all elements except el, if given
		this.blur = function(el){
			for(var d in this.cur.divs){
				if(el && (el.id == d)) continue;
				this.cur.divs[d].className = this.cur.divs[d].className.split('-')[0];
				if(Selection) Selection.remove(d);
			}
			if(!el) Selection.clear()
		}

		this.resize = function(){
			var dims = window.dimensions();
			// We only care if alteration is 20px or more
			if( (((dims.height - this.cur.height + Chrome.MCOffset.y)*1) >= 20) 
				|| (((dims.width - this.cur.width + Chrome.MCOffset.x)*1) >= 20) ){
				this.setFrameDims();
				this.redraw();
			}
		}
		
		this.selectall = function(){
			Selection.multi = true
			for(var d in this.cur.divs) this.cur.divs[d].click();
			Selection.multi = false
		}
		
		this.sorttr = {
			icon:'',
			name:'Name'
		}
		
		this.createTable = function(){
			this.table = new Table({className:'DskTable'});
			this.body.appendChild(this.table);	
		}
		
		this.switchTo = function(n){
			if(this.view == n) return;
			this.view = n;
			this.draw();
		}
		
		this.drawn = function(){}

		this.draw(1)
	}

	MainCell.divOnOver = function(e){
		if(this.className.substr(-4) != '-sel') this.className = this.className.split('-')[0] + '-mo';
	}

	MainCell.divOnOut = function(e){
		if(this.className.substr(-4) != '-sel') this.className = this.className.split('-')[0];
	}

	MainCell.divOnClick = function(e){
		if(!Selection.multi) MC.blur(this);
		// set default vars here for tidiness
		if(!this.ct) this.ct = 1;
		// click time, for handling double clicks
		var ct = new Date().getTime();
		// less than n milliseconds since last click?  Honor the intention
		if(ct < this.ct + 500){
			// triple clicks can take us to /location/location, and using arguments.callee.done 
			// means div remains locked after back button, so another time value (locktimer):
			if(this.lt > ct) return;
			this.lt = ct + 1000;
			this.app.click();
		}
		// else it's a single click
		this.ct = ct;
		Selection.set(this.app);
		this.className = this.className.split('-')[0] + '-sel';
	}

	// mkIconDiv, for icon view - child object, x/y offsets. Returns div object
	MainCell.mkIconDiv = function(c, x, y, MC){
		var d = mk('div');
		for(var k in c) d[k] = c[k];
		d.app = c
		
		d.className = 'DskApp';
		d.style.backgroundImage = "url(" + c.icon + ")";
		d.style.left = MC.offset.x + ((c.x) ? c.x : 10 + (x * 84) ) + 'px';
		d.style.top =  MC.offset.y +  ((c.y) ? c.y : 10 + (y * 84) ) + 'px';

		var da = mk('div');
		da.style.height = '32px';
		da.style.width = '32px';
		da.style.margin = '4px 19px 15px';
		if(c.overlay) da.style.background = "url(" + c.overlay + ")";
		d.appendChild(da);

		var db = mk('div');
		db.style.textAlign = 'center';
		d.appendChild(db);

		var span = mk('span');
		span.innerHTML = c.name;
		db.appendChild(span);

		d.title = c.title || c.name
		c.div = d;
		return d;
	}
	
	MainCell.mkAppTr = function(c, MC, i){
		if(!MC.table) MC.createTable();
		var tr = MC.table.tr(i, {height:36, className:'DskTr'});
		var i = 0;
		for(var k in MC.sorttr){
			k == 'icon' ? tr.td(i, {width:32}).style.backgroundImage = "url(" + c.icon + ")" : tr.td(i).innerHTML = c[k];
			i++
		}
		tr.app = c
		c.div = tr;
		return tr;
	}


/* -------------------------------------------------------------------------------------------
SELECTION

------------------------------------------------------------------------------------------- */
	
	function Selected(f){
		return (f) ? Selection.firstItem() : Selection.selected;	
	}
	
	var SelectionHandler = function(){
		this.load = function(o){
			if(o) for(k in o) this[k] = o[k]
		}
		this.selected = {};
		this.multi = false;
		this.allow_multi = false;
		this.length = 0;
		this.getlength = function(){
			this.length = 0;
			for(var k in this.selected) this.length++;
			return this.length;
		}
		this.empty = function(){
			for(var k in this.selected) return false;
			return true;
		}
		this.firstItem = function(){
			for(var k in this.selected) return this.selected[k]; 
		}
		this.set = function(k, v){
			if(!v){ v = k; k = k.id; }
			this.selected[k] = v;
			this.getlength();
			this.updated()
		}
		this.get = function(k){
			return this.selected[k];
		}
		this.clear = function(){
			this.selected = {}
			this.length = 0;
			this.updated()
		}
		this.remove = function(k){
			if(!this.selected[k]) return;
			var my = {};
			for(var a in this.selected) if(a != k) my[a] = this.selected[a];
			this.selected = my;
			this.getlength();
			this.updated()
		}
		this.index = function(){
			var a = new Array;
			for(var k in this.selected) a.push(k);
			return a
		}
		this.updated = function(){}
	}
	
	
/* -------------------------------------------------------------------------------------------
CLIPBOARD

------------------------------------------------------------------------------------------- */
	
	var ClipboardHandler = function(){
		this.load = function(o){
			if(o) for(k in o) this[k] = o[k]
		}
		this.length = 0;
		this.contents = {};
		this.action = 'copy';
		this.cut = function(){
			this.action = 'cut';
			this.setCookie('clipboard_action', 'cut');
			this.getSelection()
		}
		this.copy = function(){
			this.action = 'copy';
			this.setCookie('clipboard_action', 'copy');
			this.getSelection()
		}
		this.getSelection = function(){
			this.set(Selection.index())
		}
		this.paste = function(){
			ajaxExec('paste', {items:this.contents, action:this.action});
		}
		this.set = function(a){
			this.contents = {};
			if(typeof a == 'string') this.contents[a] = a;
			else for(var i=0; i<a.length; i++) this.contents[a[i]] = a[i];
			this.write()
		}
		this.write = function(){
			var a = new Array;
			for(var k in this.contents) a.push(k);
			this.setCookie('clipboard_content', a.join(','));
			this.length = a.length
			if(typeof Chrome.clipboard_alert == 'function') Chrome.clipboard_alert(this)
		}
		this.setCookie = function(n, v){
			$C(n).set({value:v, expires:0, path:'/'});	
			$C(n).write();
		}
		this.read = function(){
			var cc = $C('clipboard_content').value;
			var ca = $C('clipboard_action').value;
			if(ca) this.action = ca;
			if(cc) this.set(cc.split(','))
		}
		this.clear = function(){
			this.contents = {};
			this.write()
		}
		this.read();
	}
	
	
/* -------------------------------------------------------------------------------------------
TOOLBAR

------------------------------------------------------------------------------------------- */
	
	var ToolbarBuilder = function(ops){
		if(!ops) ops = {}
		this.ops = ops;
		this.el = mk('ul');
		this.el.id = 'tlb-ul0';
		this.el.className = 'tlb';
		$('tlb').innerHTML = '';
		$('tlb').appendChild(this.el);
		this.sub = {};
		this.load = function(ops){
			for(var n in ops){
				this.sub[n] = new ToolbarOp(ops[n], n);
				this.sub[n].el.className = 'tlb0';
				this.el.appendChild(this.sub[n].el);
				this.sub[n].armed(1)
			}
		}
		this.load(this.ops)
		this.option = function(n){
			if(is_null(this.sub[n])){
				this.sub[n] = new ToolbarOp({}, n);
				this.sub[n].el.className = 'tlb0';
				this.el.appendChild(this.sub[n].el);
				this.sub[n].armed(1)
			} 
			return this.sub[n];
		}
	}
	
	var ToolbarOp = function(op, n){
		this.t = 'f';
		this.o = '';
		this.readop = function(k){
			switch(true){
				case Object.isUndefined(this[k]):
					return true;
				case Object.isString(this[k]):
					return eval(this[k]);
				case Object.isFunction(this[k]):
					return this[k]();
				default:
					return (true == this[k]);
			}
		}
		this.armable = function(){
			return this.readop('e');
		}
		this.displayable = function(){
			return this.readop('d');
		}
		this.classname = function(){
			return this.isMenu() ? 'tlb-menu' : 'tlb-func' ;
		}
		this.armed = function(b){
			var armed = this.armable();
			this.span.className = this.classname();
			if(!armed) this.span.className += '-disarmed';
			this.el.style.display = (this.displayable()) ? 'block' : 'none';
			if(this.t == 'm') for(var m in self.sub) self.sub[m].armed()
			return armed;
		}
		this.doit = function(){
			var self = (typeof this.tlbOp == 'object') ? this.tlbOp : this;
			if((self.t != 'f') || (!self.armed())) return false;
			return self.readop('o');
		}
		this.mo = function(){
			var self = (typeof this.tlbOp == 'object') ? this.tlbOp : this;
			self.armed();
			if(self.t == 'm') for(var m in self.sub) self.sub[m].armed()
		}
		this.option = function(n){
			if(!this.isMenu()) this.toMenu();
			if(is_null(this.o[n])){
				this.o[n] = new ToolbarOp({}, n);
				this.ul.appendChild(this.o[n].el);
			} 
			return this.o[n];
		}
		this.toFunction = function(op){
			this.t = 'f';
			for(var k in op) this[k] = op[k];
		}
		this.toMenu = function(){
			this.t = 'm';
			if(typeof this.o !== 'object') this.o = {};
			if(!this.ul){
				this.ul = mk('ul');
				this.el.appendChild(this.ul);
				this.sub = {};
			}
		}
		this.isMenu = function(){
			return this.t == 'm';
		}
		for(var k in op) this[k] = op[k];
		this.el = mk('li');
		this.span = mk('span');
		this.span.innerHTML = n;
		this.span.tlbOp = this;
		this.el.appendChild(this.span);
		if(this.isMenu()){
			this.toMenu();
			for(var n in this.o){
				this.sub[n] = new ToolbarOp(this.o[n], n);
				this.ul.appendChild(this.sub[n].el);
			}
		}
		addEvent(this.span, 'mouseover', this.mo);
		addEvent(this.span, 'click', this.doit);
	}


/* -------------------------------------------------------------------------------------------
	TUX
	
	------------------------------------------------------------------------------------------- */
	
	var TUX = {
		status:'quiet',
		stati:['quiet','appearing','speaking','fading'],
		asserting:false,
		get busy(){
			return (TUX.asserting || ['apearing','fading'].include(TUX.status));	
		},
		get speaking(){
			return ['apearing','speaking'].include(TUX.status);
		},
		transitionEvents:{
			appearing:function(){
				$('spkbk').show();
				Chrome.mask('TUX');
				TUX.status = 'appearing';	
			},
			appeared:function(){
				// let him get a word in
				$('spk').show()
				setTimeout(function(){ 
					addEvent($('msk'), 'click', TUX.bequiet);
					TUX.status = 'speaking';
				}, 500);
			},
			fading:function(){
				$('spk').hide()
				removeEvent($('msk'), 'click', TUX.bequiet);
				TUX.status = 'fading';	
			},
			faded:function(){
				$('spkbk').hide();
				Chrome.unmask('TUX');
				TUX.clear();
				TUX.status = 'quiet';
				TUX.asserting = false	
			}
		},
		transitions:{
			fadeIn:function(){
				var dims = $('spk').getDimensions()
				new Effect.Morph($('spkbk'),
					{
						style:{
							height:dims.height+'px',
							width:dims.width+'px'
						},
						duration:0.3,
						fps:30,
						beforeStart:TUX.transitionEvents.appearing,
						afterFinish:TUX.transitionEvents.appeared
					}
				);
			},
			fadeOut:function(){
				new Effect.Morph($('spkbk'),
					{
						style:{
							height:'0px',
							width:'0px'
						},
						duration:0.3,
						fps:30,
						beforeStart:TUX.transitionEvents.fading,
						afterFinish:TUX.transitionEvents.faded
					}
				);
			}
		},
		phrases:{
			'yo':'wattup?'
		},
		learn:function(phrase, dialogue){
			this.phrases[phrase] = dialogue;	
		},
		forget:function(phrase){
			this.phrases[phrase] = false;
		},
		recall:function(phrase, options){
			return (typeof this.phrases[phrase] == 'function') ? this.phrases[phrase](options) : this.phrases[phrase];
		},
		say:function(phrase, options){
			if(!this.phrases[phrase]) return this.justsay("I don't know how to say '"+phrase+"'");
			this.interject(this.recall(phrase, options))
		},
		justsay:function(sentence){
			this.interject(sentence);
		},
		interject:function(dialogue){
			if(this.busy) return setTimeout(function(){ TUX.interject(dialogue) }, 500);
			this.clear();
			if(typeof dialogue == 'string')
				this.Elements.spkwrap.innerHTML = dialogue;
			else
				try{
					this.Elements.spkwrap.appendChild(dialogue.self)
				}catch(e){ return };
			this.transitions.fadeIn()
		},
		clear:function(){
			if(TUX.Elements.spkwrap.firstChild){
				if(TUX.dialogue) TUX.dialogue.destroy()
				TUX.Elements.spkwrap.removeChild(TUX.Elements.spkwrap.firstChild)
			}
			TUX.Elements.spkwrap.innerHTML = '';	
		},
		click:function(e){
			if(e && e.keyCode != 13) return;
			if(TUX.dialogue && TUX.dialogue.defaultButton) return TUX.dialogue.defaultButton.click()
			TUX.bequiet()
		},
		get dialogue(){
			if(TUX.Elements.spkwrap.firstChild && TUX.Elements.spkwrap.firstChild.creator) return TUX.Elements.spkwrap.firstChild.creator
			return false
		},
		speak:function(){
			if(!TUX.busy) TUX.say('hello');
		},
		bequiet:function(){
			if(!TUX.busy) TUX.transitions.fadeOut();
			if(TUX.asserting) $('upbg').flash('orange');
		}
	}	
	
	TUX.input = Class.create({
		initialize:function(options){
			this.creator = this
			this.options={
				type:'text',
				className:'tuxinp',
				value:'',
				onchange:Prototype.emptyFunction
			}
			Object.extend(this.options, options || {});
			this.self = mk('input', {creator:this});
			Object.extend(this.self, this.options)
			Event.observe(this.self, 'change', this.change.bind(this.self));
		},
		change:function(){
			this.creator.flash();
			if(this.self.onchange) this.self.onchange()
		},
		flash:function(color){
			if(this.flashing || !this.self.id) return
			this.flashing = true
			if(!color) color = '#ffff99'
			new Effect.Morph(this.self.id, {
				style: 'background:'+color,
				duration: 0.3,
				afterFinish:this.creator.flashOff.bind(this)
			});
		},
		flashOff:function(){
			new Effect.Morph(this.self.id, {
				style: 'background:#ffffff;',
				duration: 0.3,
				afterFinish:this.creator.flashDone.bind(this)
			});
		},
		flashDone:function(){
			this.flashing = false
		},
		set value(v){
			this.self.value = v
		},
		set onchange(f){
			this.self.onchange = f
		},
		set:function(options){
			Object.extend(this.self, options)
		},
		destroy:function(){
			Event.stopObserving(this.self, 'change', this.change)
		}
	});
	
	TUX.button = Class.create({
		initialize:function(options){
			this.creator = this
			this.options={
				value:'OK',
				target:Prototype.emptyFunction,
				onSubmit:Prototype.emptyFunction,
				type:'button',
				className:'tuxbtn',
				send:true
			}
			Object.extend(this.options, options || {});
			this.self = mk('input', {creator:this});
			Object.extend(this.self, this.options)
			Event.observe(this.self, 'click', this.click.bind(this.self))
		},
		click:function(){
			this.creator.flash();
			this.self.target(typeof this.self.send == 'function' ? this.self.send() : this.self.send)
			this.self.onSubmit();
		},
		flash:function(){
			if(this.flashing) return
			this.flashing = true
			this.self.style.color = 'green';
			setTimeout(function(){ 
				this.self.style.color = "#222222"
				this.flashing = false
			}.bind(this), 300);
		},
		set:function(options){
			Object.extend(this.self, options)
		},
		destroy:function(){
			Event.stopObserving(this.self, 'click', this.click)
		}
	});
	
	TUX.Bubble = Class.create({
		initialize: function(options) {
			this.options={
				header:'',
				body:'',
				footer:'',
				target:TUX.bequiet,
				onSubmit:TUX.bequiet,
				buttons:{OK:true},
				defaultButton:'OK',
				inputs:false
			};
			this.inputs = {}
			this.buttons = {}
			
			Object.extend(this.options, options || {});
			this.self = mk('div', {
				id:'tuxbubble',			  
				creator:this
			});	
			this.header = this.self.mk('h3', {
				id:'tuxheader',
				innerHTML:this.options.header								
			});
			this.body = this.self.mk('div', {
				id:'tuxbody',
				innerHTML:this.options.body,
				creator:this					
			});
			this.footer = this.self.mk('div', {
				id:'tuxfooter',
				innerHTML:this.options.footer
			});
			if(this.options.inputs){
				this.body.innerHTML += '<br /><br /><br /><br /><br /><br /><br /><br />'
				for(id in this.options.inputs){
					ops = this.options.inputs[id];
					if(typeof ops !== 'object'){
						ops = {}
						ops.value = this.options.inputs[id]
					}
					ops.id = id;
					this.inputs[id] = new TUX.input(ops)
					this.body.mk('div', {className:'tuxinpwrap'}).appendChild(this.inputs[id].self)
				}
			}
			if(this.options.buttons){
				var btnwrap = this.body.mk('div', {className:'tuxbtnwrap'});
				for(value in this.options.buttons){
					ops = this.options.buttons[value];
					if(typeof ops !== 'object'){
						ops = {}
						ops.send = this.options.buttons[value]
					}
					ops.value = value
					if(!ops.onSubmit) ops.onSubmit = this.options.onSubmit
					if(!ops.target) ops.target = this.options.target
					this.buttons[value] = new TUX.button(ops)
					btnwrap.appendChild(this.buttons[value].self)
				}
				if(this.buttons[this.options.defaultButton]) this.defaultButton = this.buttons[this.options.defaultButton]
			}
		},
		append:function(str){
			this.footer.innerHTML = str;
			TUX.transitions.fadeIn()
		},
		hasInputs:function(){
			for(var k in this.inputs) return true
			return false
		},
		destroy:function(){
			for(k in this.buttons) this.buttons[k].destroy()
			for(k in this.inputs) this.inputs[k].destroy()
			this.destroyed = true
		}
	});
	
	var TUXalert = function(sentence, callback, options){
		options = options || {}
		options.body = sentence
		if(callback) options.target = callback
		var dialogue = new TUX.Bubble(options);
		TUX.interject(dialogue);
		return dialogue;
	}
	
	var TUXconfirm = function(sentence, callback, options){
		options = options || {}
		options.body = sentence
		if(!options.buttons) options.buttons = {OK:1, Cancel:0};
		if(callback) options.target = callback
		var dialogue = new TUX.Bubble(options);
		TUX.interject(dialogue);
		return dialogue;
	}
	
	var TUXprompt = function(sentence, suggest, callback, options){
		options = options || {}
		options.body = sentence
		if(is_null(suggest)) suggest = ''
		if(!options.inputs) options.inputs = {tuxprompt:suggest}
		if(!options.buttons) options.buttons = {OK:function(){ return $('tuxprompt').value }, Cancel:0};
		if(callback) options.target = callback
		var dialogue = new TUX.Bubble(options);
		TUX.interject(dialogue);
		return dialogue;
	}
	
	TUX.phrases.hello = function(){
		var s = '<div align="left">'
		+ '<div style="min-height:140px">'
		+ '<img src="' + Chrome.icon_uri((usr().is_root ? 'Root' : 'User') + '.png', 128) + '" style="float:left; margin:-5px 5px 5px -5px" />'
		+ '<h1>Hey, ' + usr().name + '!</h1>'
		+ '<br /><br />You logged in at ' + UnixToTime(usr().last_login) + '<br />';
		if(usr()['class'] == 'Sudoer' && usr().is_root){
			s += '<small>Your sudo session expires in ' + Math.floor( ((usr().sudo_expires*1000)-new Date().getTime())/60000) 
			+ ' minutes</small><br />'; 
		}
		s += '</div><div style="padding-left:40px">'
		+ 'Tasks:<div style="padding-left:20px">'
		+ '-> <a href="javascript:TUX.say(\'authenticate\');">switch user</a><br />'
		+ '-> <a href="javascript:TUX.say(\'usurpas\');">view usurpas</a><br />'
		+ '-> <a href="javascript:TUX.say(\'pubkey\');">view pubkey</a><br />'
		+ '-> <a href="javascript:Smajax.ping(1)">ping server</a><br />'
		+ '</div></div><br /><br /></div>';	
		return new TUX.Bubble({body:s})
	}
	
	TUX.phrases.authenticate = function(){
		// Get the salt
		if(!Chrome.salt) getsalt();
		// Set the callback
		var ops = {}
		ops.onSubmit = Prototype.emptyFunction
		ops.target = function(){
			var n = $('uname').value.trim();
			var p = $('upass').value.trim();
			if(!n || !p){
				$('upbg').flash('orange');
				return;
			}
			Smajax.login(n, hex_sha1(hex_sha1(p)+Chrome.salt));
		}
		ops.header = 'Login:';
		ops.inputs = {uname:usr().name, upass:{type:'password'}}
		if(Smernel.secure) ops.buttons = {OK:1,Cancel:{target:TUX.bequiet}}
		else $('upbg').flash('orange');
		return new TUX.Bubble(ops)
	}
	
	TUX.phrases.pubkey = function(){
		var s = '<div align="left">'
		+ '<div style="min-height:140px">'
		+ '<h3>Session Public Key</h3>'
		+ '<br /><small>Not to be confused with an RSA public key (it\'s a misnomer, sorry), this token allows you to see usurpas from the home domain</small>'
		+ '<br /><br />Currently: '+pubkey()+'<br /><br />'
		+ '<small>Your cookie should already be set in this browser.  '
		+ 'To set it in another, copy and paste the address below into the browser\'s address bar:</small><br /><br />'
		+ '<input type="text" readonly="readonly" style="width:400px" onclick="this.select()" value="http://'+DOM.name+'/?pubkey='+pubkey()+'&set_pubkey_and_exit=1" />'
		+ '<br /><br /><small>To view the site without usurpas, at any address set/append smuoi_usurpas=off to the query string, e.g:<br /><br />'
		+ '<a href="http://'+DOM.name+'/?smuoi_usurpas=off" target="_blank">http://'+DOM.name+'/?smuoi_usurpas=off</a> '
		+ '<br /><br />(or just sign out from Smuoi)</small>'
		+ '</div>'
		return new TUX.Bubble({body:s})
	}
	
	TUX.phrases.usurpas = function(){
		Smajax.exec('usurpas', {}, '/');
	}
			
	window.queue(function(){
		if(!usr()) return	
		TUX.Elements = (function(){
			var o = {
				spk:$('spk'),
				spkbk:$('spkbk'),
				spktable:new Table({width:'100%'}),
				upng:$('upng'),
				upbg:$('upbg')
			}
			Object.extend($('spkbk').style, {width:'0px',height:'0px'});
			o.spktd = o.spktable.tr().td(0, {id:'spktd', align:'center', valign:'middle', 'height':300, 'width':400});
			o.spktd.valign = 'center';
			o.spkwrap = mk('div', {id:'spkwrap'});
			o.spktd.appendChild(o.spkwrap);
			o.spk.appendChild(o.spktable);
			var mu = function(){
				if(!TUX.speaking) $('upbg').style.opacity = 0.3;									 
			};
			addEvent(o.upng, 'mouseover', mu);
			addEvent(o.upng, 'mousedown', function(){
				if(!TUX.speaking) $('upbg').style.opacity = 0.6;									 
			});
			addEvent(o.upng, 'click', function(){
				if(!TUX.speaking) TUX.speak();									 
			});
			addEvent(o.upng, 'mouseup', mu);
			addEvent(o.upng, 'mouseout', function(){
				if(!TUX.speaking) $('upbg').style.opacity = 0;									 
			});
			o.upbg.flash = function(color){
				if(this.flashing) return;
				var args = {pulses:3, duration:1.5};
				if(color){
					var oldColor = this.style.backgroundColor;
					args.afterFinish = function(){ 
						$('upbg').style.backgroundColor = oldColor 
						$('upbg').flashing = false;
					};
					this.flashing = true;
					this.style.backgroundColor = color;
				}
				this.pulsate(args);
			}
			return o;
		})();
		TUX.new_user = function(){
			TUX.asserting = false
			TUX.bequiet();
			$('upng').style.backgroundImage = 'url(' + Chrome.icon_uri(((usr().is_root) ? 'Root.png' : 'User.png')) + ')';	
			if(usr().fresh) TUX.say('hello');
		}
		TUX.new_user()
		Smernel.observe('loggedin', TUX.new_user);
		Student = TUX;
		say = TUX.say.bind(TUX), learn = TUX.learn.bind(TUX), forget = TUX.forget.bind(TUX), justsay = TUX.justsay.bind(TUX);
		Event.observe(document, 'keydown', TUX.click)
		Smernel.callAuthHandler = function(){
			TUX.say('authenticate');	
			TUX.asserting = !Smernel.secure;
		}
	});
	

/* -------------------------------------------------------------------------------------------
NAMER

------------------------------------------------------------------------------------------- */
	
	var Namer = {
				
		begin:function(id){
			if(!id) id = O_ID;
			if(!id) return;
			this.id = id;
			this.json = false
			this.overwrite = false
			this.names = []
			justsay('<h3>wait...</h3><br /><div class="wait"></div>');
			Smajax.exec('findme', {id:id}, '/@Nicename', this.prompt.bind(this));
		},
		
		prompt:function(json){
			this.json = json;
			this.names = json.details.names
			var name = '';
			var body = '<small>Omit the "my" prefix</small><br /><br />';
			if(this.names.length){
				name = this.names[0]
				body += 'current names:<ol>'
				this.names.each(function(name){ body += '<li>' + name + '</li>' });
				body += '</ol><br />';
			}
			this.dialogue = new TUX.Bubble(
				{
					header:'Give this resource a Nice Name:',
					body:body,
					inputs:{nicename:name},
					buttons:{OK:function(){ $('nicename').creator.change(); return $('nicename').value }, Cancel:{target:TUX.bequiet}},
					onSubmit:Prototype.emptyFunction,
					target:this.submit.bind(this)
				}
			);
			this.input = this.dialogue.inputs['nicename'].self;
			this.OK = this.dialogue.buttons['OK'].self;
			this.input.onchange = Namer.clean.bind(this.input)
			TUX.interject(this.dialogue)
		},
		
		submit:function(n){
			if(!n || this.names.include(n)) return;
			var o = (!this.dooverwrite(n) || this.json.code == 2) ? '' : 1;
			this.dialogue.append('');
			Smajax.exec('nameme', {id:O_ID,name:n,overwrite:o}, '/@Nicename', this.update.bind(this));
		},
		
		dooverwrite:function(n){
			return (n && this.overwrite == n)	
		},
		
		update:function(json){
			this.json = json
			switch(json.code){
				case 3:
					this.overwrite = this.input.value;
					var s = '<div align="left">The name "' + json.details.name + '" already exists (' + json.details.typename + '). Overwrite?</div>';
					break;
				case 1:
					var s = 'You named the resource.  Hooray!';
					this.OK.target = TUX.bequiet
					break;
				default:
					var s = 'the resource was not created, perhaps an error';
			}
			this.dialogue.append(s);
		},
		
		clean:function(){
			// Strangely, a single call to replace non-alphanumeric characters doesn't do the trick, so a while loop is used
			while(this.value.match(/[^A-Za-z0-9_\.]/)) this.value = this.value.replace(/[^A-Za-z0-9_\.]/, '');
			while(this.value.match(/\.\./)) this.value = this.value.replace(/\.\./, '.');
			while(this.value.substr(0, 1) == '.') this.value = this.value.substr(1)
			while(this.value.substr(-1) == '.') this.value = this.value.substr(0, this.value.length-1)
		}
	}
	
	
/* -------------------------------------------------------------------------------------------
BROWSER

------------------------------------------------------------------------------------------- */

	var Browser = {
				
		built:false,
		
		init:function(){
			if(this.built) return;
			this.div = mk('div');
			this.div.id = 'Browser';
			$U('body').appendChild(this.div);
			this.div.innerHTML = 
				'<div id="BrowserTop">'
					+ '<span id="BrowserTitle"></span>'
					+ '<a href="javascript:Browser.hide()" id="BrowserClose">Close Window</a>'
				+ '</div>'
				+ '<div id="BrowserTlb"></div>'
				+ '<div id="BrowserCell"></div>'
				+ '<div id="BrowserBase"><div id="hbtns"></div><div id="sops" style="display:none"><input type="text" id="brinp" value="" onchange="$(\'brbtn\').disabled = !(this.value)" /> <input type="button" id="brbtn" value="save" onclick="Browser.onselect()" /></div></div>';
			this.titl = $('BrowserTitle');
			this.tlbr = $('BrowserTlb');
			this.cell = $('BrowserCell');
			this.base = $('BrowserBase');
			this.inp = $('brinp');
			this.btn = $('brbtn')
			Smernel.fire('browser:loaded');
			this.built = true;
		},
				
		new_session:function(){
			this.init();
			this._CACHE = {};
			this.location = '/';
			this.task = '';
			this.btntxt = 'save';
			this.input = '';
			this.xargs = {};
			this.onselect = function(){};
			this.Apps = false;
			this.history = new BHistory();
			this.clearscreen();
		},
		
		load:function(o){
			if(!this.built) this.new_session();
			for(var k in o) this[k] = o[k];
			this.titl.innerHTML = this.title
			this.inp.value = this.input;
			this.btn.value = this.btntxt;
			$('sops').style.display = (this.input || this.btntext) ? 'block' : 'none';
		},
		
		proceedto:function(b){
			if(this.location.substr(-1) != '/') b  = '/' + b;
			this.goto(this.location + b);
			this.history.showbuttons();
		},
		
		goto:function(L){
			if(!this.built) this.new_session();
			if(this.is_cached(L)){
				if(this.location == L) return this.appear();
				return this.loadcache(L)
			}
			this.location = L;
			this.history.reindex()
			this.go();
		},
		
		go:function(){
			ajaxExec('browse', this.xargs, this.location);
		},
		
		receive:function(d){
			this.docache(d);
			if(typeof d == 'string') return this.loadHTML(d);
			this.loadApps(d);
		},
		
		loadHTML:function(h){
			this.cell.innerHTML = h;
			this.appear();
		},
		
		loadApps:function(a){
			if(!this.Apps) this.Apps = AppsB;
			if(!this.MC) this.MC = new BrowserCell;
			this.Apps.load(a);
			this.MC.draw();
			this.appear();
		},
		
		loadTools:function(t){
			this.tlbr.innerHTML = this.history.buttons() + t;
		},
		
		docache:function(data){
			this._CACHE[this.location] = data;	
		},
		
		loadcache:function(L){
			this.location = L
			this.receive(this._CACHE[L]);
			this.history.reindex();
		},
		
		is_cached:function(L){
			return (Boolean)(this._CACHE[L]);
		},
		
		appear:function(){
			if(this.active) return;
			Chrome.mask('Browser');
			this.cell.style.background = '#fff';
			this.cell.style.opacity = 1;
			this.div.show();
			this.active = true;
		},
		
		clearscreen:function(){
			this.active = false;
			this.div.hide();
			//Browser.cell.innerHTML = '';
			this.cell.style.background = '#fff url(' + Chrome.icon_uri('wait.gif', 32) + ') center no-repeat';
			this.cell.style.opacity = 0.7;
		},
		
		hide:function(){
			Chrome.unmask('Browser');
			Browser.div.fade({duration:0.5, afterFinish:Browser.clearscreen.bind(Browser)});
		}
		
	}
	
	var AppsB = {}
	Object.extend(AppsB, Apps);


/* -------------------------------------------------------------------------------------------
BROWSER HISTORY

------------------------------------------------------------------------------------------- */

	var BHistory = function(){
		
		this.i = 0;
		
		this.h = [];
	
		this.reindex = function(){
			var n = this.getIndexOf(Browser.location);
			if(n == -1) return this.write(Browser.location);
			this.setindex(n);
		}
		
		this.getIndexOf = function(L){
			for(var n=0; n<this.h.length; n++) if(this.get(n) == L) return n;
			return -1;
		}
	
		this.write = function(L){
			if(this.iskey(this.i+1) && (this.get(this.i+1) !== L)) this.forget();
			this.h.push(L);
			this.setindex(this.h.length-1)
		}
		
		this.forward = function(n){
			if(!n) n=1;
			if(!this.iskey(this.i+n)) return;
			this.setindex(this.i+n);
			Browser.goto(this.cur());
		}
		
		this.back = function(n){
			if(!n) n=1;
			if(!this.iskey(this.i-n)) return;
			this.setindex(this.i-n);
			Browser.goto(this.cur());
		}
		
		this.setindex = function(n){
			this.i = n;
			if($('fbtn')) $('fbtn').disabled = (!this.iskey(n+1));
			if($('bbtn')) $('bbtn').disabled = (!this.iskey(n-1));
		}
		
		this.cur = function(){
			return this.get(this.i);	
		}
		
		this.get = function(n){
			return this.h[n];	
		}
		
		this.iskey = function(n){
			return !(typeof this.h[n] == 'undefined');	
		}
		
		this.forget = function(){
			var a = [];
			for(var n=0; n<=this.i; n++) a[n] = this.h[n];
			this.h = a;
		}
		
		this.showbuttons = function(){
			this.mkbuttons();
			$('hbtns').show()
		}
		
		this.mkbuttons = function(){
			if($('bbtn')) return;
			var div = $('hbtns');
			div.style.display = 'none';
			var bbtn = mk('input');
			bbtn.type = 'button';
			bbtn.id = 'bbtn';
			bbtn.value = 'back';
			//bbtn.disabled = true;
			addEvent(bbtn, 'click', function(){
				Browser.history.back(1);
			});
			div.appendChild(bbtn);
			div.appendChild(document.createTextNode(' '));
			var fbtn = mk('input');
			fbtn.type = 'button';
			fbtn.id = 'fbtn';
			fbtn.value = 'forward';
			fbtn.disabled = true;
			addEvent(fbtn, 'click', function(){
				Browser.history.forward(1);
			});
			div.appendChild(fbtn)
		}
	}
	
	
/* -------------------------------------------------------------------------------------------
BROWSER CELL


The methods that follow are more or less a copy of the MainCell model, but are duplicated
for the convenience of keeping their key differences distinct
------------------------------------------------------------------------------------------- */

	var BrowserCell = function(){
		this.body = $('BrowserCell')
		this.Apps = Browser.Apps
		// cur
		this.cur = {}
		this.cur.divs = {}
		this.cur.width = 600;
		this.cur.height = 440;

		// xy : calculates maximum number of files that may be fitted into current dimensions.
		// extends frame size downward if neccesary
		this.xy = function(){
			var w = this.cur.width - 20;
			var h = this.cur.height - 20;
			// maximum divs we can fit horizontally
			var mx = (w - (w % 84)) / 84;
			// maximum divs we can fit vertically
			var my = (h - (h % 84)) / 84;
			// maximum divs we can fit on screen
			var max = mx * my;
			// is there enough space? if not, expand y
			var l = this.Apps.length;
			if (l > (mx * my)) my = my + parseInt((l-max)/mx) + 1 ;
			return {x:mx, y:my}
		}

		// draw : creates new div elements, adds their event listeners and appends them to body
		this.draw = function(){
			this.cur.divs = {};
			var i = 0;
			var xy = this.xy();
			var A = this.Apps.apps;
			for(var id in A){
				var x = (i < xy.y) ? 0 : parseInt(i/xy.y);
				var y = (i < xy.y) ? i : (i % xy.y);
				i++;
				this.cur.divs[id] = this.mkIconDiv(A[id], x, y);
			}
			this.body.innerHTML = '';
			for(var d in this.cur.divs){
				this.body.appendChild(this.cur.divs[d]);
				addEvent(this.cur.divs[d], 'mouseover', BrowserCell.divOnOver);
				addEvent(this.cur.divs[d], 'mouseout', BrowserCell.divOnOut);
				addEvent(this.cur.divs[d], 'click', BrowserCell.divOnClick);
			}
		}
		
		// blur : deselects all elements except el, if given
		this.blur = function(el){
			for(var d in this.cur.divs){
				if(el && el == this.cur.divs[d]) continue;
				this.cur.divs[d].className = 'DskApp';
			}
		}
		
		this.mkIconDiv = function(c, x, y){
			var d = mk('div');
			for(var k in c) d[k] = c[k];
			if(d.disabled){
				d.style.opacity = 0.3
				d.style.cursor = 'default';
			}
			d.app = c
			
			d.className = (!d.highlighted) ? 'DskApp' : 'DskApp-hl';
			d.style.backgroundImage = "url(" + c.icon + ")";
			d.style.left = ((c.x) ? c.x : 10 + (x * 84) ) + 'px';
			d.style.top =  ((c.y) ? c.y : 10 + (y * 84) ) + 'px';
	
			var da = mk('div');
			da.style.height = '54px';
			d.appendChild(da);
	
			var db = mk('div');
			db.style.textAlign = 'center';
			d.appendChild(db);
	
			var span = mk('span');
			span.innerHTML = c.name;
			db.appendChild(span);
	
			return d;
		}
	}

	BrowserCell.divOnOver = function(e){
		if(this.highlighted) return;
		this.className = 'DskApp-mo';
	}

	BrowserCell.divOnOut = function(e){
		if(this.highlighted) return;
		this.className = 'DskApp';
	}

	BrowserCell.divOnClick = function(e){
		if(this.disabled || this.highlighted) return;
		// set default vars here for tidiness
		if(!this.ct) this.ct = 1;
		// click time, for handling double clicks
		var ct = new Date().getTime();
		// less than n milliseconds since last click?  Honor the intention
		if(ct < this.ct + 500){
			// triple clicks can take us to /location/location, and using arguments.callee.done 
			// means div remains locked after back button, so another time value (locktimer):
			if(this.lt > ct) return;
			this.lt = ct + 1000;
			this.app.click();
		} 
		// else it's a single click
		this.ct = ct;
		this.className = 'DskApp-sel';
	}

